1. useReducer

从后面的学习中可以看到,useReducer的使用很麻烦,使用起来和redux类似,难度也和redux同一个级别的,而且我感觉useReducer还是会经常用到的,比redux会频繁得多,所以刚开始看不懂不要紧,多看几遍。

当状态更新逻辑较复杂时可以考虑使用 useReducer。useReducer 可以同时更新多个状态,而且能把对状态的修改从组件中独立出来。

什么是reducer呢?

image-20231128154954492

image-20231128160954042

相比于 useState,useReducer 可以更好的描述“如何更新状态”。例如:组件负责发出行为,useReducer 负责更新状态。

好处是:让代码逻辑更清晰,代码行为更易预测。

1.1 useReducer 的语法格式

useReducer 的基础语法如下:

其中:

image-20231128161818305

  1. reducer 是一个函数,类似于 (prevState, action) => newState。形参 prevState 表示旧状态,形参 action 表示本次的行为,返回值 newState 表示处理完毕后的新状态。
  2. initState 表示初始状态,也就是默认值。
  3. initAction 是进行状态初始化时候的处理函数,它是可选的,如果提供了 initAction 函数,则会把 initState 传递给 initAction 函数进行处理,initAction 的返回值会被当做初始状态。
  4. 返回值 state 是状态值。dispatch 是更新 state 的方法,让它接收 action 作为参数,useReducer 只需要调用 dispatch(action) 方法传入的 action 即可更新 state。

1.2 useReducer 的基础用法

1. 定义组件的基础结构

  1. 定义名为 Father 的父组件如下:

  2. 定义名为 Son1Son2 的两个子组件如下:

  3. index.css 中添加对应的样式:

2. 定义 useReducer 的基础结构

  1. 按需导入 useReducer 函数:

  2. 定义初始数据

  3. 定义 reducer 函数,它的作用是:根据旧状态,进行一系列处理,最终返回新状态

  4. Father 组件中,调用 useReducer(reducerFn, 初始状态) 函数,并得到 reducer 返回的状态:

  5. 为 reducer 中的 initState 指定数据类型:

    接下来,在 Father 组件中使用 state 时,就可以出现类型的智能提示啦:

3. 使用 initAction 处理初始数据

  1. 定义名为 initAction 的处理函数,如果初始数据中的 age 为小数、负数、或 0 时,对 age 进行非法值的处理:

  2. Father 组件中,使用步骤1声明的 initAction 函数如下:

    可以在定义 defaultState 时,为 age 提供非法值,可以看到非法值在 initAction 中被处理掉了。

4. 在 Father 组件中点击按钮修改 name 的值

4.1 错误示范
4.2 正确的操作
  1. 为了能够触发 reducer 函数的重新执行,我们需要在调用 useReducer() 后接收返回的 dispatch 函数。示例代码如下:

  2. 在 button 按钮的点击事件处理函数中,调用 dispatch() 函数,从而触发 reducer 函数的重新计算:

  3. 点击 Father 组件中如下的 button 按钮(如图所示):

    修改 name 的值会触发 reducer 函数的重新执行,并打印 reducer 中的 console.log(),代码如下:

    触发了reducer函数,并且组件rerender。

4.4 调用 dispatch 传递参数给 reducer
  1. 在 Father 父组件按钮的点击事件处理函数 onChangeName 中,调用 dispatch() 函数并把参数传递给 reducer 的第2个形参,代码如下:

  2. 修改 reducer 函数的形参,添加名为 action 的第2个形参,用来接收 dispatch 传递过来的数据:

  3. 在 reducer 中,根据接收到的 action.type 标识符,决定进行怎样的更新操作,最终 return 一个计算好的新状态。示例代码如下:

  4. 在上述的 switch...case... 代码期间,没有任何 TS 的类型提示,这在大型项目中是致命的。因此,我们需要为 reducer 函数的第2个形参 action 指定操作的类型:

    同时,在 Father 组件的 onChangeName 处理函数内,调用 dispatch() 时也有了类型提示:

    注意:在今后的开发中,正确的顺序是先定义 ActionType 的类型,再修改 reducer 中的 switch…case… 逻辑,最后在组件中调用 dispatch() 函数哦!这样能够充分利用 TS 的类型提示。

5. 把用户信息渲染到子组件中

  1. 在 Father 父组件中,通过展开运算符把 state 数据对象绑定为 Son1Son2 的 props 属性:

  2. 在子组件中,指定 props 的类型为 React.FC<UserType>,并使用 props 接收和渲染数据:

    修改完成后,点击父组件中的 button 按钮修改用户名,我们发现两个子组件中的数据同步发生了变化。

6. 在子组件中实现点击按钮 age 自增操作

  1. 扩充 ActionType 的类型如下:

  2. reducer 中添加 INCREMENTcase 匹配:

  3. 在子组件 Son1 中添加 +1 的 button 按钮,并绑定点击事件处理函数:

  4. 现在的问题是:子组件 Son1 中无法调用到父组件的 dispatch 函数。为了解决这个问题,我们需要在 Father 父组件中,通过 props 把父组件中的 dispatch 传递给子组件:

  5. Son1 子组件中,扩充 React.FC<UserType> 的类型,并从 props 中把 dispatch用户信息对象分离出来:

7. 在子组件中实现点击按钮 age 自减操作

  1. 扩充 ActionType 的类型如下:

  2. reducer 中添加 DECREMENTcase 匹配:

  3. 在子组件 Son2 中添加 -5 的 button 按钮,并绑定点击事件处理函数:

  4. 现在的问题是:子组件 Son2 中无法调用到父组件的 dispatch 函数。为了解决这个问题,我们需要在 Father 父组件中,通过 props 把父组件中的 dispatch 传递给子组件:

  5. Son2 子组件中,扩充 React.FC<UserType> 的类型,并从 props 中把 dispatch用户信息对象分离出来:

8. 在 GrandSon 组件中实现重置按钮

  1. 扩充 ActionType 的类型如下:

  2. reducer 中添加 RESETcase 匹配:

  3. GrandSon 组件中,添加重置按钮,并绑定点击事件处理函数:

    其实搞清楚了整体构造和流程,使用起来还是蛮简单的,下一步就应该是将代码拆分,构造一个自定义hook,引入useReducer,然后把reducer和action分开到单独的文件中,使用的时候再引入。

    说一万遍不如做一遍,下面是上面案例,我的自定义hook做法:

    记录一种报错:

    image-20231129142252836

     

    这样做的好处就是:关注点分离。逻辑写好之后,只需要调用就行了,否则UI和逻辑混在一起,非常难理解。

9. 使用 Immer 编写更简洁的 reducer 更新逻辑

  1. 安装 immer 相关的依赖包:

  2. use-immer 中导入 useImmerReducer 函数,并替换掉 React 官方的 useReducer 函数的调用:

  3. 修改 reducer 函数中的业务逻辑,case 代码块中不再需要 return 不可变的新对象了,只需要在 prevState 上进行修改即可。Immer 内部会复制并返回新对象,因此降低了用户(react的用户就是程序员)的心智负担。改造后的 reducer 代码如下:

2. useContext

在 react 函数式组件中,如果组件的嵌套层级很深,当父组件想把数据共享给最深层的子组件时,传统的办法是使用 props一层一层把数据向下传递

使用 props 层层传递数据的维护性太差了,我们可以使用 React.createContext() + useContext() 轻松实现多层组件的数据传递。

image-20230920202508702

2.1 useContext 的语法格式

主要的使用步骤如下:

  1. 全局创建 Context 对象。变量对象名一般取为XxxxContext的形式,因为这个变量是要用作标签的,所以首字母要大写。初始数据一般定义为空对象。
  2. 父组件中使用 <XxxxContext.Provider value={}>{/**...各种子组件*/}</XxxxContext.Provider> 提供数据
  3. 子组件中使用 useContext(XxxxContext) 接收数据。

执行效果:

image-20231211091626147

 

2.2 useContext 的基础用法

1. 定义组件结构

定义 LevelALevelBLevelC 的组件结构如下:

2. createContext 配合 useContext 使用

在父组件中,调用 React.createContext 向下共享数据;在子组件中调用 useContext() 获取数据。示例代码如下:

重要:

如果不知道react中某些变量的类型,特别是以前从来没有见过、用过的API,该怎么办?

1、首先是查文档,看文档里面是怎么定义的。

2、如果什么文档都查不到,那么就看vscode里面的提示,这个提示还是蛮有用的,先按照提示写上去,等有问题再来改。

image-20231211093759313

如果要我查找文档的话,我还真不知道该怎么查。这时候可以将鼠标放在setCount上面,会有类型的提示:

不敢保证vscode的提示100%正确,但是先把提示的类型写上去,有问题再改,这也是个好方法。

3、react的API定义的变量类型,要区分是返回值类型还是参数类型,确实很复杂,但再复杂也要搞清楚。

比如说:

这里的React.FC是返回值类型,<>里面的是参数类型,这应该是typescript的基础知识,只不过我一直没有使用,所以很不熟悉。

-----2024.02.22

还是要会看官方typescript定义,鼠标选中某个react的方法,右键选择“Go to Type Definition”,就可以跳转到官方定义的文件里面去:

image-20240222150144181

会发现有两个同名的类型定义,这是为什么呢?

image-20240222150359539

上面一个是右键点击“Go to Definition”会高亮的地方,下面一个是右键点击“Go to Type Definition”会高亮的地方,这个应该是函数重载,真正的定义应该在“Go to source definition”跳转到的位置,但是我看了一下,跳转到的位置的函数定义也是很简短,不知道经过什么方式处理了。

3. ☆☆☆以非侵入的方式使用 Context

在刚才的案例中,我们发现父组件 LevelA 为了向下传递共享的数据,在代码中侵入了 <AppContext.Provider> 这样的代码结构。

为了保证父组件中代码的单一性,也为了提高 Provider 的通用性,我们可以考虑把 Context.Provider 封装到独立的 Wrapper 函数式组件中,将Wrapper组件做成一个单独的传递值的组件,所有子组件都要用到的值,只定义在Wrapper组件中,这样就可以做到业务与数据分离(当然实际情况也许没有这么完美,子组件中完全可以自定义值、获取数据来使用),例如:

这里的AppContextWrapper参数类型也不好定义,怎么办?无论鼠标指向props,还是props.children,都没有很好的提示:

------2024.02.23

原因是什么呢?原因是我没有在AppContextWrapper组件的FC类型里面指定泛型定义,写成这样就会有提示了:

 

定义好 Wrapper 组件后,我们可以在 App.tsx 中导入并使用 WrapperLevelA 组件,代码如下:

这样,组件树的嵌套关系为:App => Wrapper => LevelA => LevelB => LevelC。因此在 LevelALevelBLevelC 组件中,都可以使用 context 中的数据。例如,LevelA 组件中的代码如下:

LevelC 组件中的代码如下:

核心思路:每个 Context 都创建一个对应的 Wrapper 组件,在 Wrapper 组件中使用 Provider 向 children 注入数据。

 

4. 使用 useContext 重构 useReducer 案例

  1. 定义 Context 要向下共享的数据的 TS 类型,代码如下:

  2. 使用 React.createContext 创建 Context 对象:

  3. 创建 ContextWrapper 组件如下,把 Father 组件中的 useImmerReducer 调用过程,抽离到 ContextWrapper 中:

  4. 改造 Father 组件,调用 useContext 获取并使用 Context 中的数据。同时,Father 组件也不必再使用 props 把 statedispatch 函数传递给 Son 子组件:

  5. 改造 App 根组件,分别导入 UserInfoContextWrapperFather 组件,并形成父子关系的嵌套,这样 Father 组件及其子组件才可以访问到 Context 中的数据:

  6. 最后,改造 Son1Son2GrandSon 组件,删除 props 及其类型定义,改用 useContext() 来获取 UserInfoContextWrapper 向下注入的数据。示例代码如下:

完整版代码如下:

执行效果: